TabView

Scripting provides a modern Tab system aligned with iOS 18+:

  • TabView — container that manages multiple tabs and switching between them
  • Tab — a single tab and its associated content
  • TabSection — a way to group tabs into sections, each with its own configuration and header

Combined with TabView-level options and TabViewCustomization, this enables rich tab layouts, including sidebar representations, customization, and persistence.

This document focuses on:

  • How to structure tab content using TabView, Tab, and TabSection
  • How to configure tab bar and sidebar behaviors
  • How to use TabViewCustomization to persist and restore user customizations

1. Basic Usage: TabView + Tab

In the simplest case, TabView hosts multiple Tab elements. Each Tab defines:

  • A title and system image for the tab item
  • A value used for selection
  • An optional role (for example search)
  • The actual view content
1import { TabView, Tab, useObservable } from 'scripting'
2
3function RootView() {
4  const selection = useObservable<number>(0)
5
6  return (
7    <TabView selection={selection}>
8      <Tab
9        title="Home"
10        systemImage="house.fill"
11        value={0}
12      >
13        <HomeView />
14      </Tab>
15
16      <Tab
17        title="Search"
18        systemImage="magnifyingglass"
19        value={1}
20        role="search"
21      >
22        <SearchView />
23      </Tab>
24
25      <Tab
26        title="Settings"
27        systemImage="gearshape.fill"
28        value={2}
29      >
30        <SettingsView />
31      </Tab>
32    </TabView>
33  )
34}

Key points:

  • TabView selection={selection} binds the current tab to an observable value.
  • Each Tab’s value must match the observable’s type (number or string).
  • Tabs with role="search" integrate with tabViewSearchActivation behavior (see below).

2. Grouping Tabs with TabSection

When you have many tabs, or when you want a sidebar-like structure, use TabSection to group related tabs.

The structure becomes:

1TabView
2 ├─ TabSection
3 │   ├─ Tab
4 │   ├─ Tab
5 │   └─ ...
6 ├─ TabSection
7 │   ├─ Tab
8 │   └─ ...

2.1 Using title as a section header

1function MailRootView() {
2  const selection = useObservable<string>('inbox')
3
4  return (
5    <TabView selection={selection}>
6      <TabSection title="Mailboxes">
7        <Tab
8          title="Inbox"
9          systemImage="tray.full.fill"
10          value="inbox"
11        >
12          <InboxView />
13        </Tab>
14
15        <Tab
16          title="Sent"
17          systemImage="paperplane.fill"
18          value="sent"
19        >
20          <SentView />
21        </Tab>
22      </TabSection>
23
24      <TabSection title="Labels">
25        <Tab
26          title="Important"
27          systemImage="star.fill"
28          value="important"
29        >
30          <ImportantView />
31        </Tab>
32      </TabSection>
33    </TabView>
34  )
35}

2.2 Using header for a custom section header

If you need a richer header (icon + text + description, etc.), use header instead of title:

1<TabSection
2  header={
3    <HStack spacing={8}>
4      <Image systemName="folder.fill" />
5      <VStack alignment="leading">
6        <Text fontWeight="bold">Projects</Text>
7        <Text fontSize={12} foregroundColor="secondary">
8          Recently opened projects
9        </Text>
10      </VStack>
11    </HStack>
12  }
13>
14  <Tab title="Project A" systemImage="doc.fill" value="projectA">
15    <ProjectAView />
16  </Tab>
17
18  <Tab title="Project B" systemImage="doc.fill" value="projectB">
19    <ProjectBView />
20  </Tab>
21</TabSection>

title and header are mutually exclusive: use one or the other per section.


3. Section-Level Configuration: Layout, Actions, Drag & Drop

TabSection can control how a section is presented and how it behaves.

3.1 tabPlacement

Controls where and how the section’s tabs appear. Common values:

  • automatic — let the system decide based on environment.
  • pinned — pins tabs so they remain visible in the bar.
  • sidebarOnly — show tabs only in the sidebar representation.

Example: a section that only appears in the sidebar:

1<TabSection
2  title="Tags"
3  tabPlacement="sidebarOnly"
4>
5  <Tab title="Important" systemImage="star.fill" value="important">
6    <ImportantView />
7  </Tab>
8</TabSection>

3.2 sectionActions

Provides extra actions associated with a section, such as “Add” or “More”.

1<TabSection
2  title="Lists"
3  sectionActions={
4    <Button
5      title="Add"
6      systemImage="plus"
7      action={addNewList}
8    />
9  }
10>
11  <Tab title="Today" systemImage="sun.max.fill" value="today">
12    <TodayView />
13  </Tab>
14</TabSection>

3.3 Visibility and customization behavior

At the section level you can configure:

  • Default visibility in different placements (tab bar, sidebar)
  • Customization behavior (whether users can reorder or adjust the section)

Typical use-case: a section that users can reorder in a Tab layout editor:

1<TabSection
2  title="Files"
3  customizationID="files-section"
4  customizationBehavior="reorderable"
5>
6  <Tab title="Recent" systemImage="clock.fill" value="recent">
7    <RecentFilesView />
8  </Tab>
9</TabSection>

3.4 Drag & drop integration

Both TabSection and Tab can participate in drag & drop via:

  • draggable — logical drag identifier
  • dropDestination — handler for dropped items

Example:

1<TabSection
2  title="Files"
3  draggable="files-section"
4  dropDestination={items => handleDroppedItems(items)}
5>
6  <Tab title="Recent" systemImage="clock.fill" value="recent">
7    <RecentFilesView />
8  </Tab>
9</TabSection>

4. TabView-Level Configuration

On the TabView (or the view owning the TabView) you can configure global behavior such as:

  • Tab bar minimization
  • Bottom accessories
  • Search activation behavior
  • Sidebar header/footer/bottom bar
  • Customization state (tabViewCustomization)

4.1 tabBarMinimizeBehavior (iOS 26.0+)

Controls how the tab bar minimizes in response to scrolling:

  • automatic
  • never
  • onScrollDown
  • onScrollUp
1<TabView
2  selection={selection}
3  tabBarMinimizeBehavior="onScrollDown"
4>
5  {/* sections + tabs */}
6</TabView>

4.2 tabViewBottomAccessory (iOS 26.0+)

Places a view at the bottom of the TabView—below the tab bar or tab area.

1<TabView
2  selection={selection}
3  tabViewBottomAccessory={
4    <HStack spacing={8}>
5      <Text fontSize={12}>Swipe left or right to switch tabs</Text>
6      <Spacer />
7      <Button title="Got it" action={dismissHint} />
8    </HStack>
9  }
10>
11  {/* sections + tabs */}
12</TabView>

4.3 tabViewSearchActivation (iOS 26.0+)

Configures how search is activated for tabs with role="search":

  • automatic
  • searchTabSelection — activate search when the search tab is selected
1<TabView
2  selection={selection}
3  tabViewSearchActivation="searchTabSelection"
4>
5  <Tab title="Home" systemImage="house.fill" value="home">
6    <HomeView />
7  </Tab>
8
9  <Tab
10    title="Search"
11    systemImage="magnifyingglass"
12    value="search"
13    role="search"
14  >
15    <SearchView />
16  </Tab>
17</TabView>

4.4 Sidebar-specific views (iOS 18.0+)

For sidebar-style TabView, you can add:

  • tabViewSidebarHeader — top area (user info, app logo, etc.)
  • tabViewSidebarFooter — bottom area (settings, logout)
  • tabViewSidebarBottomBar — bar between main content and bottom edge
1<TabView
2  selection={selection}
3  tabViewSidebarHeader={
4    <VStack alignment="leading" spacing={4}>
5      <Image systemName="person.circle.fill" fontSize={32} />
6      <Text fontWeight="bold">User Name</Text>
7      <Text fontSize={12} foregroundColor="secondary">
8        Welcome back
9      </Text>
10    </VStack>
11  }
12  tabViewSidebarFooter={
13    <Button title="Settings" systemImage="gearshape" action={openSettings} />
14  }
15  tabViewSidebarBottomBar={
16    <Button title="Upgrade to Pro" systemImage="star.fill" action={upgrade} />
17  }
18>
19  {/* sections + tabs */}
20</TabView>

5. TabViewCustomization: Persisting Layout and Visibility

TabViewCustomization is the core object that represents the customization state of a TabView. It can:

  • Track section order
  • Track tab order within each section
  • Track tab visibility (tab bar vs sidebar)
  • Reset section order or visibility
  • Be serialized to / from Data for persistence

The typical pattern is:

  1. Initialize TabViewCustomization from storage (if present), otherwise create a new instance.
  2. Observe changes to it and save serialized data back to storage.
  3. Use it to query and modify section and tab customizations.
  4. Pass it into the TabView via tabViewCustomization.

5.1 Initializing and persisting TabViewCustomization

Below is the correct example using useObservable and Storage:

1const customization = useObservable<TabViewCustomization>(() => {
2  const data = Storage.get('tab_customization')
3  if (data) {
4    return TabViewCustomization.fromData(data) ?? new TabViewCustomization()
5  }
6  return new TabViewCustomization()
7})
8
9useEffect(() => {
10  const listener = (newValue: TabViewCustomization) => {
11    const data = newValue.toData()
12    if (data) {
13      Storage.set('tab_customization', data)
14    }
15  }
16  customization.subscribe(listener)
17  return () => {
18    customization.unsubscribe(listener)
19  }
20}, [])

Explanation:

  • The initializer:

    • Reads raw Data from Storage using the key tab_customization.
    • Uses TabViewCustomization.fromData(data) to recreate a customization object.
    • Falls back to new TabViewCustomization() if the data is invalid or missing.
  • The useEffect:

    • Subscribes to changes on the observable.
    • Every time the TabViewCustomization changes, toData() is called and persisted.
    • Cleans up the subscription on unmount.

This ensures the layout is restored on launch and any user changes are saved automatically.

5.2 Using TabViewCustomization with TabView

You typically pass the observable itself into the TabView:

1<TabView
2  selection={selection}
3  tabViewCustomization={customization}
4>
5  {/* TabSection + Tab structure */}
6</TabView>

Internally, the Tab system updates the TabViewCustomization object as the user edits the layout, reorders sections, hides tabs, and so on. The observable subscription persists these updates.

5.3 Working with sections: getSection and section order

You can query a section by its customizationID:

1const filesSection = customization.value.getSection('files-section')

A section customization can:

  • Expose tabOrder: the array of tab IDs in this section (or null if not customized).
  • Provide resetTabOrder(): to restore the original system-defined order of tabs in this section.

Example:

1function resetFilesSectionOrder() {
2  const section = customization.value.getSection('files-section')
3  section?.resetTabOrder()
4}

5.4 Working with tabs: getTab and visibility

You can query a tab by its customizationID:

1const importantTab = customization.value.getTab('important-tab')

A tab customization exposes:

  • tabBarVisibility — read-only current visibility in the tab bar.
  • sidebarVisibility — read/write visibility in the sidebar representation.

Example: hiding a tab from the sidebar only:

1const importantTab = customization.value.getTab('important-tab')
2if (importantTab) {
3  importantTab.sidebarVisibility = 'hidden'
4}

This allows you to:

  • Implement “show/hide in sidebar” toggles.
  • Sync visibility with user preferences or other settings.

5.5 Global resets: section order and visibility

Two convenience methods reset parts of the customization:

1customization.value.resetSectionOrder()
2customization.value.resetVisibility()

Typical usage: a “Reset layout” button.

1<Button
2  title="Restore Default Layout"
3  action={() => {
4    customization.value.resetSectionOrder()
5    customization.value.resetVisibility()
6  }}
7/>

This restores both:

  • Section ordering
  • Tab visibility (in tab bar and sidebar)

to their original default state.


6. Relationship with tabItem-based API

Earlier examples in the project may use a tabItem view modifier to configure tab labels. That approach is documented elsewhere and is suitable for simple Tab views.

However, for:

  • Grouped tabs (TabSection)
  • Sidebar representations
  • Tab reordering and visibility customization (TabViewCustomization)
  • Per-section actions and layouts

you should use the TabView + Tab + TabSection + TabViewCustomization structure described here.

It provides a clearer model, matches modern iOS Tab APIs, and is designed to work seamlessly with customization and persistence.